﻿using System.Collections.Generic;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using DeepEarth.Core;
using DeepEarth.Core.Controls;
using DeepEarth.Core.Utilities;
using DeepEarth.Map.Core;
using DeepEarth.Toolkit.Common;
using DeepEarth.Toolkit.Drawing;
using DeepEarth.Toolkit.Geometry.Data;
using System.Windows.Media;
using System.Linq;
using System;
using DeepEarth.Toolkit.Controls;

namespace DeepEarth.Toolkit.Controls
{   
    [TemplatePart(Name = PART_PointButton, Type = typeof(ToggleButton))]
    [TemplatePart(Name = PART_LabelButton, Type = typeof(ToggleButton))]
    [TemplatePart(Name = PART_FreeDrawButton, Type = typeof(ToggleButton))]
    [TemplatePart(Name = PART_EnableEditButton, Type = typeof(ToggleButton))]
    [TemplatePart(Name = PART_ClearButton, Type = typeof(Button))]
    public class Digitizer : MapInteractionControl
    {
        const string PART_PointButton = "PART_PointButton";
        const string PART_LabelButton = "PART_LabelButton";
        const string PART_FreeDrawButton = "PART_FreeDrawButton";
        const string PART_EnableEditButton = "PART_EnableEditButton";
        const string PART_ClearButton = "PART_ClearButton";

        ToggleButton pointButton;
        ToggleButton labelButton;
        ToggleButton freeDrawButton;
        ToggleButton enableEditButton;

        public ICommand AddPushpinCommand { get; set; }
        public ICommand AddLabelCommand { get; set; }
        public ICommand FreeDrawCommand { get; set; }
        public ICommand EnableEditCommand { get; set; }
        public ICommand ClearCommand { get; set; }
        public ICommand ObjectDrawnCommand { get; set; }

        KeyEventHandler keyHandler;

        FreeDraw freeDraw;
        ShapeEditor shapeEditor;

        #region Dependency Properties
        public static readonly DependencyProperty DrawnObjectsProperty = DependencyProperty.Register("DrawnObjects", typeof(DrawnObjectsCollection), typeof(Digitizer), null);
        public DrawnObjectsCollection DrawnObjects
        {
            get { return (DrawnObjectsCollection)GetValue(DrawnObjectsProperty); }
            set { SetValue(DrawnObjectsProperty, value); }
        }

        public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register("Orientation", typeof(Orientation), typeof(Digitizer), new PropertyMetadata(Orientation.Horizontal));
        public Orientation Orientation
        {
            get { return (Orientation)GetValue(OrientationProperty); }
            set { SetValue(OrientationProperty, value); }
        }

        public static readonly DependencyProperty GeometryConverterProperty = DependencyProperty.Register("GeometryConverter", typeof(IValueConverter), typeof(Digitizer), new PropertyMetadata(new DefaultDrawnGeometryConverter()));
        /// <summary>
        /// Geometry converters are used to convert the "base" geometry user draw on the map 
        /// into the "real" business objects.
        /// </summary>
        /// <remarks>
        /// <para>
        ///  <see cref="IValueConverter"/> instances convert "base" geometry types used draw on the map into
        /// the "real" business model/view objects.
        /// </para>
        /// <para>
        /// When user draws shapes or adds pushpins, it really means something in terms of the business logic of the application.
        /// For example, on the map displaying bicycle stations each pushpin user adds probably means "a new station is here".
        /// So it make sense to have (and display on the map) a "BicycleStation" object, which may have its own template, etc, 
        /// rather than "standard" Geometry.Pushpin.
        /// </para>
        /// <para>
        /// In Runtime user can event switch between different kind of objects (BicycleStation, PetrolStation, MyBicycleWasStolenHere, etc).
        /// Technically it can be done by changing the concrete geometry converter.
        /// </para>
        /// </remarks>
        public IValueConverter GeometryConverter
        {
            get { return (IValueConverter)GetValue(GeometryConverterProperty); }
            set { SetValue(GeometryConverterProperty, value); }
        }

        public static readonly DependencyProperty EditEnabledProperty = DependencyProperty.Register("EditEnabled", typeof(bool), typeof(Digitizer), new PropertyMetadata(true, EnableEditChanged));
        public bool EditEnabled
        {
            get { return (bool)GetValue(EditEnabledProperty); }
            set { SetValue(EditEnabledProperty, value); }
        }

        static void EnableEditChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            var self = (Digitizer)sender;

            var newValue = (bool)args.NewValue;

            if (!newValue)
            {
                if (self.currentlyEditing)
                {
                    self.EndEdit();
                }
            }

            ((LambdaCommand)self.ClearCommand).IsEnabled = newValue;
        }

        public static readonly DependencyProperty EnableEditButtonCheckedImpliesEditableProperty = DependencyProperty.Register("EnableEditButtonCheckedImpliesEditable", typeof(bool), typeof(Digitizer), new PropertyMetadata(true));
        public bool EnableEditButtonCheckedImpliesEditable
        {
            get { return (bool)GetValue(EnableEditButtonCheckedImpliesEditableProperty); }
            set { SetValue(EnableEditButtonCheckedImpliesEditableProperty, value); }
        }

        public static readonly DependencyProperty PointMarkerProperty = DependencyProperty.Register("PointMarker", typeof(FrameworkElement), typeof(Digitizer), new PropertyMetadata(OnPointMarkerChanged));
        public FrameworkElement PointMarker
        {
            get { return (FrameworkElement)GetValue(PointMarkerProperty); }
            set { SetValue(PointMarkerProperty, value); }
        }

        static void OnPointMarkerChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            var self = (Digitizer)sender;

            if (self.pushpinDrawProcessor != null)
            {
                self.pushpinDrawProcessor.Icon = args.NewValue as FrameworkElement;
            }
        }

        public static readonly DependencyProperty PointMarkerPositionOriginProperty = DependencyProperty.Register("PointMarkerPositionOrigin", typeof(PositionOrigin), typeof(Digitizer), new PropertyMetadata(OnPointMarkerPositionOriginChanged));
        public PositionOrigin PointMarkerPositionOrigin
        {
            get { return (PositionOrigin)GetValue(PointMarkerPositionOriginProperty); }
            set { SetValue(PointMarkerPositionOriginProperty, value); }
        }

        static void OnPointMarkerPositionOriginChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            var self = (Digitizer)sender;

            if (self.pushpinDrawProcessor != null && self.PointMarker != null)
            {
                self.pushpinDrawProcessor.PositionOrigin = (PositionOrigin)args.NewValue;
            }
        }

        public static readonly DependencyProperty StrokeProperty = DependencyProperty.Register("Stroke", typeof(Brush), typeof(Digitizer), new PropertyMetadata(new SolidColorBrush(Colors.Orange), OnStrokeChanged));
        public Brush Stroke
        {
            get { return (Brush)GetValue(StrokeProperty); }
            set { SetValue(StrokeProperty, value); }
        }

        static void OnStrokeChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            Digitizer self = (Digitizer)sender;

            self.SyncColours();
        }

        public static readonly DependencyProperty FillProperty = DependencyProperty.Register("Fill", typeof(Brush), typeof(Digitizer), new PropertyMetadata(new SolidColorBrush(Color.FromArgb(64, 255, 204, 153)), OnFillChanged));
        public Brush Fill
        {
            get { return (Brush)GetValue(FillProperty); }
            set { SetValue(FillProperty, value); }
        }

        static void OnFillChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            Digitizer self = (Digitizer)sender;

            self.SyncColours();
        }

        public static readonly DependencyProperty ActiveGeometryProperty = DependencyProperty.Register("ActiveGeometry", typeof(Geometry.Data.Geometry), typeof(Digitizer), new PropertyMetadata(OnActiveGeometryChanged));
        public Geometry.Data.Geometry ActiveGeometry
        {
            get { return (Geometry.Data.Geometry)GetValue(ActiveGeometryProperty); }
            set { SetValue(ActiveGeometryProperty, value); }
        }

        static void OnActiveGeometryChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            Digitizer self = (Digitizer)sender;

            if (args.NewValue is ShapeGeometry)
            {
                ShapeGeometry sg = (ShapeGeometry)args.NewValue;
            }
        }

        public static readonly DependencyProperty ModeProperty = DependencyProperty.Register("Mode", typeof(DigitizerMode), typeof(Digitizer), new PropertyMetadata(DigitizerMode.Idle, OnModeChanged));
        public DigitizerMode Mode
        {
            get { return (DigitizerMode)GetValue(ModeProperty); }
            set { SetValue(ModeProperty, value); }
        }
        static void OnModeChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            // This handler will cause the abort add point and end interaction calls to occur twice.
            // Not a problem but clearly not ideal.
            Digitizer self = (Digitizer)sender;

            var oldMode = (DigitizerMode)args.OldValue;
            var newMode = (DigitizerMode)args.NewValue;

            if (newMode == DigitizerMode.Idle)
            {
                if (oldMode == DigitizerMode.Pushpin)
                {
                    self.AbortAddPoint();
                }
                else if (oldMode == DigitizerMode.Label)
                {
                    self.AbortAddLabel();
                }
                else if (oldMode == DigitizerMode.FreeDraw)
                {
                    self.AbortFreeDraw();
                }
                
                self.UnToggle();
            }            
        }

        void SyncColours()
        {
            if (this.freeDraw != null)
            {
                this.freeDraw.Stroke = Stroke;
                this.freeDraw.Fill = Fill;
            }

            if (this.labelDrawProcessor != null)
            {
                this.labelDrawProcessor.Stroke = Stroke;
            }

            if (this.ActiveGeometry != null && ActiveGeometry is ShapeGeometry)
            {
                ShapeGeometry sg = this.ActiveGeometry as ShapeGeometry;

                sg.Stroke = Stroke;
                sg.Fill = Fill;
            }
        }

        #endregion

        public Digitizer()
        {
            DefaultStyleKey = typeof(Digitizer);

            AddPushpinCommand = new LambdaCommand(BeginAddPoint);
            AddLabelCommand = new LambdaCommand(BeginAddLabel);
            FreeDrawCommand = new LambdaCommand(BeginFreeDraw);
            
            EnableEditCommand = new LambdaCommand(() =>
                {
                    if (enableEditButton != null)
                    {
                        EditEnabled = enableEditButton.IsChecked ?? false;

                        if (!EnableEditButtonCheckedImpliesEditable)
                        {
                            EditEnabled = !EditEnabled;
                        }
                    }
                    else
                    {
                        EditEnabled = !EditEnabled;
                    }
                }
            );

            ClearCommand = new LambdaCommand(() =>
                {
                    if (EditEnabled)
                    {
                        if (currentlyEditing)
                        {
                            EndEdit();
                        }

                        DrawnObjects.Clear();
                    }
                }
            );

            DrawnObjects = new DrawnObjectsCollection();
            DrawnObjects.CollectionChanged += OnGeometryCollectionChanged;

            keyHandler = Digitizer_KeyDown;
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            pointButton = (ToggleButton)GetTemplateChild(PART_PointButton);
            labelButton = (ToggleButton)GetTemplateChild(PART_LabelButton);
            freeDrawButton = (ToggleButton)GetTemplateChild(PART_FreeDrawButton);
            enableEditButton = (ToggleButton)GetTemplateChild(PART_EnableEditButton);
        }

        void OnGeometryCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    if (!GeoItems.Contains(DrawnObjects))
                        GeoItems.Add(DrawnObjects);
                    break;
                default:
                    if (DrawnObjects.Count == 0 && GeoItems.Contains(DrawnObjects))
                        GeoItems.Remove(DrawnObjects);
                    break;
            }
        }

        protected override void GeoItemsChanged(GeoItems newGeo, GeoItems oldGeo)
        {
            if (oldGeo != null && oldGeo.Contains(DrawnObjects))
                oldGeo.Remove(DrawnObjects);
            if (newGeo != null && DrawnObjects.Count > 0)
                newGeo.Add(DrawnObjects);
        }

        void BeginInteraction()
        {
            if (currentlyEditing)
            {
                EndEdit();
            }

            Map.InteractionSurface.IsHitTestVisible = true;

            if (!Map.OverlayPanel.Children.Contains(drawLayer))
            {
                Map.OverlayPanel.Children.Add(drawLayer);
            }
            Application.Current.RootVisual.AddHandler(KeyDownEvent, keyHandler, true);
        }

        void EndInteraction()
        {
            Map.OverlayPanel.Cursor = null;
            Map.OverlayPanel.ReleaseMouseCapture();
            Map.InteractionSurface.IsHitTestVisible = false;
            Map.OverlayPanel.Children.Remove(drawLayer);

            Application.Current.RootVisual.RemoveHandler(KeyDownEvent, keyHandler);
        }

        DynamicLayer drawLayer = new DynamicLayer();

        void BeginFreeDraw()
        {
            if (freeDrawButton.IsChecked ?? false)
            {
                pointButton.IsChecked = false;
                AbortAddPoint();

                labelButton.IsChecked = false;
                AbortAddLabel();

                BeginInteraction();

                Map.OverlayPanel.Cursor = Cursors.Stylus;

                freeDraw = new FreeDraw(Map.InteractionSurface, drawLayer, Map.LocationToViewportPoint, Map.ViewportPointToLocation);
                freeDraw.Stroke = Stroke;
                freeDraw.Fill = Fill;

                var weakListener = new WeakEventListener<Digitizer, FreeDraw>(this, freeDraw);
                freeDraw.DrawComplete += weakListener.OnEvent;

                weakListener.DetatchAction = (listener, source) =>
                {
                    source.DrawComplete -= listener.OnEvent;
                };

                weakListener.EventAction = (listener, source, eventArgs) => listener.CommitFreeDraw();

                freeDraw.BeginFreeDraw();

                shapeEditor = new ShapeEditor(Map.InteractionSurface, drawLayer, Map.LocationToViewportPoint, Map.ViewportPointToLocation);
                shapeEditor.Edit(freeDraw.FreeDrawModel.Locations);

                ActiveGeometry = freeDraw.FreeDrawModel;

                Mode = DigitizerMode.FreeDraw;
            }
            else
            {
                CommitFreeDraw();

                Mode = DigitizerMode.Idle;
            }
        }

        void CommitFreeDraw()
        {
            if (freeDraw.FreeDrawModel.Locations.Count >= 2)
            {
                AddDrawnObject(freeDraw.FreeDrawModel.Clone());

                Mode = DigitizerMode.Idle;
            }
            AbortFreeDraw();
        }

        void AbortFreeDraw()
        {
            ActiveGeometry = null;

            if (freeDraw != null)
            {
                freeDraw.Clear();
                freeDraw.EndFreeDraw();
            }

            if (freeDrawButton != null && freeDrawButton.IsChecked.Value)
            {
                freeDrawButton.IsChecked = false;
            }

            EndInteraction();
        }

        public void SaveCurrent()
        {
            if (freeDraw != null)
            {
                CommitFreeDraw();
            }
        }

        PushpinDrawProcessor pushpinDrawProcessor;

        void BeginAddPoint()
        {
            if (pointButton.IsChecked ?? false)
            {
                labelButton.IsChecked = false;
                AbortAddLabel();

                freeDrawButton.IsChecked = false;
                AbortFreeDraw();
                BeginInteraction();

                pushpinDrawProcessor = new PushpinDrawProcessor(Map.InteractionSurface, drawLayer, Map.LocationToViewportPoint, Map.ViewportPointToLocation);

                if (PointMarker != null)
                {
                    pushpinDrawProcessor.Icon = PointMarker;
                    pushpinDrawProcessor.PositionOrigin = PointMarkerPositionOrigin;
                }

                var weakListener = new WeakEventListener<Digitizer, PushpinDrawProcessor>(this, pushpinDrawProcessor);
                pushpinDrawProcessor.DrawComplete += weakListener.OnEvent;

                weakListener.DetatchAction = (listener, source) =>
                {
                    source.DrawComplete -= listener.OnEvent;
                };

                weakListener.EventAction = (listener, source, eventArgs) =>
                {
                    var e = (DrawProcessorEventArgs)eventArgs;
                    if (e.Geometry == null)
                    {
                        listener.AbortAddPoint();
                        listener.pointButton.IsChecked = false;
                    }
                    else
                    {
                        listener.AddDrawnObject(e.Geometry);
                        listener.ActiveGeometry = e.Geometry;
                    }
                };

                var weakRightClickListener = new WeakEventListener<Digitizer, PushpinDrawProcessor>(this, pushpinDrawProcessor);
                pushpinDrawProcessor.RightClick += weakRightClickListener.OnEvent;

                weakRightClickListener.DetatchAction = (listener, source) =>
                    {
                        source.RightClick -= listener.OnEvent;
                    };

                weakRightClickListener.EventAction = (listener, source, eventArgs) =>
                    {
                        var e = (DrawProcessorMouseEventArgs)eventArgs;
                        listener.OnProcessorRightClick(e.ViewportPoint, e.Location);
                    };

                pushpinDrawProcessor.BeginDraw();

                Mode = DigitizerMode.Pushpin;
            }
            else
            {
                AbortAddPoint();
                Mode = DigitizerMode.Idle;
            }
        }

        // TODO: Similar to AbortFreeDraw. Merge.
        void AbortAddPoint()
        {
            if (pushpinDrawProcessor != null)
            {
                pushpinDrawProcessor.Clear();
            }

            EndInteraction();
        }


        LabelDrawProcessor labelDrawProcessor;

        void BeginAddLabel()
        {
            if (labelButton.IsChecked ?? false)
            {
                freeDrawButton.IsChecked = false;
                pointButton.IsChecked = false;
                AbortFreeDraw();
                AbortAddPoint();
                BeginInteraction();

                labelDrawProcessor = new LabelDrawProcessor(Map.InteractionSurface, drawLayer, Map.LocationToViewportPoint, Map.ViewportPointToLocation);
                labelDrawProcessor.Stroke = Stroke;

                var weakListener = new WeakEventListener<Digitizer, LabelDrawProcessor>(this, labelDrawProcessor);
                labelDrawProcessor.DrawComplete += weakListener.OnEvent;

                weakListener.DetatchAction = (listener, source) =>
                {
                    source.DrawComplete -= listener.OnEvent;
                };

                weakListener.EventAction = (listener, source, eventArgs) =>
                {
                    var e = (DrawProcessorEventArgs)eventArgs;
                    if (e.Geometry == null)
                    {
                        listener.AbortAddLabel();
                        listener.labelButton.IsChecked = false;
                    }
                    else
                    {
                        listener.AddDrawnObject(e.Geometry);
                        listener.ActiveGeometry = e.Geometry;
                    }
                };

                var weakRightClickListener = new WeakEventListener<Digitizer, LabelDrawProcessor>(this, labelDrawProcessor);
                labelDrawProcessor.RightClick += weakRightClickListener.OnEvent;

                weakRightClickListener.DetatchAction = (listener, source) =>
                    {
                        source.RightClick -= listener.OnEvent;
                    };

                weakRightClickListener.EventAction = (listener, source, eventArgs) =>
                    {
                        var e = (DrawProcessorMouseEventArgs)eventArgs;
                        listener.OnProcessorRightClick(e.ViewportPoint, e.Location);
                    };

                labelDrawProcessor.BeginDraw();

                Mode = DigitizerMode.Label;
            }
            else
            {
                AbortAddLabel();
                Mode = DigitizerMode.Idle;
            }
        }

        // TODO: Similar to AbortFreeDraw. Merge.
        void AbortAddLabel()
        {
            if (labelDrawProcessor != null)
            {
                labelDrawProcessor.Clear();
            }

            EndInteraction();
        }


        // Use the digitizer to edit an existing shape
        bool currentlyEditing;

        public void BeginEdit(IList<Location> locations)
        {
            if (currentlyEditing)
            {
                EndEdit();
            }

            if (EditEnabled)
            {
                currentlyEditing = true;
                Map.OverlayPanel.Children.Add(drawLayer);
                shapeEditor = new ShapeEditor(Map.InteractionSurface, drawLayer, Map.LocationToViewportPoint, Map.ViewportPointToLocation);
                shapeEditor.Edit(locations);

                Application.Current.RootVisual.AddHandler(KeyDownEvent, keyHandler, true);

                Mode = DigitizerMode.Edit;
            }
        }

        public void EndEdit()
        {
            ActiveGeometry = null;
            currentlyEditing = false;

            if (shapeEditor != null)
            {
                shapeEditor.End();
            }

            Map.OverlayPanel.Children.Remove(drawLayer);

            Application.Current.RootVisual.RemoveHandler(KeyDownEvent, keyHandler);

            Mode = DigitizerMode.Idle;
        }

        void Digitizer_KeyDown(object sender, KeyEventArgs e)
        {
            switch (e.Key)
            {
                case Key.Left :
                    Map.Pan(-200, 0);
                    break;

                case Key.Right :
                    Map.Pan(200, 0);
                    break;

                case Key.Up :
                    Map.Pan(0, -150);
                    break;

                case Key.Down :
                    Map.Pan(0, 150);
                    break;

                case Key.Escape :
                    AbortFreeDraw();
                    AbortAddPoint();
                    EndEdit();
                    EndInteraction();

                    freeDrawButton.IsChecked = false;
                    pointButton.IsChecked = false;
                    break;
            }
        }

        /// <summary>
        /// Adds the "result" drawn object to the collection
        /// </summary>
        /// <param name="geometry">A "raw" drawn object ("basic" geometry).</param>
        public void AddDrawnObject(Geometry.Data.Geometry geometry)
        {
            //Try to convert the "basic" shape into the "real" business object
            var objectToAdd = (GeometryConverter != null)
                ? GeometryConverter.Convert(geometry, typeof(object), null, null)
                : geometry;

            if (objectToAdd is IRemovable)
            {
                var r = objectToAdd as IRemovable;
                r.RemoveCommand = new LambdaCommand(() =>
                {
                    if (EditEnabled)
                    {
                        EndEdit();
                        DrawnObjects.Remove(r);
                    }
                }
                );
            }

            if (objectToAdd is ShapeGeometry)
            {
                var s = objectToAdd as ISelectable;

                s.SelectionCommand = new LambdaCommand(() =>
                {
                    var shape = (ShapeGeometry)s;
                    ActiveGeometry = shape;
                    BeginEdit(shape.Locations);     
                }
                );
            }

            else if (objectToAdd is PointGeometry)
            {
                var s = objectToAdd as ISelectable;

                s.SelectionCommand = new LambdaCommand(() =>
                {
                    ActiveGeometry = (PointGeometry)objectToAdd;
                }                    
                );
            }

            // If something has to happen to the object before it is added, do that here.
            if (ObjectDrawnCommand != null)
            {
                ObjectDrawnCommand.Execute(objectToAdd);
            }

            //Add the object to the result collection.             
            DrawnObjects.Add(objectToAdd);
        }

        void UnToggle()
        {
            if (pointButton != null && pointButton.IsChecked.Value)
            {
                pointButton.IsChecked = false;
            }

            if (freeDrawButton != null && freeDrawButton.IsChecked.Value)
            {
                freeDrawButton.IsChecked = false;
            }
        }

        const double radius = 10;
        void OnProcessorRightClick(Point rightClickPoint, Location rightClickLocation)
        {
            var drawn = DrawnObjects.OfType<ILocatable>().Where(l => IsInProximity(rightClickPoint, Map.LocationToViewportPoint(l.Position), radius)).ToList();

            foreach (var d in drawn)
                DrawnObjects.Remove(d);   
        }

        bool IsInProximity(Point a, Point b, double buffer)
        {
            double dX = b.X - a.X;
            double dY = b.Y - a.Y;

            double dist = Math.Sqrt(Math.Pow(dX, 2) + Math.Pow(dY, 2));

            return dist <= buffer;
        }
    }
}